// Package local is a file system backed store
package file

import (
	log "gitee.com/tgodfather/misc/logger"
	"go.uber.org/zap"

	//"encoding/json"
	//"os"
	//"path/filepath"
	//"sort"
	"strings"
	"sync"
	"time"

	"github.com/micro/go-micro/v2/store"
	//bolt "go.etcd.io/bbolt"
)

var (
	// DefaultDatabase is the namespace that the bbolt store
	// will use if no namespace is provided.
	DefaultDatabase = "data"
	// DefaultTable when none is specified
	DefaultTable = "table"
	// DefaultDir is the default directory for bbolt files
	//DefaultDir = filepath.Join(os.TempDir(), "data", "store")
	DefaultDir = "./data/store"
	// bucket used for data storage
	dataBucket = "data"
)

// NewStore returns a memory store
func NewStore(opts ...store.Option) store.Store {
	s := &fileStore{
		handles: make(map[string]*fileHandle),
	}
	//s.init(opts...)
	return s
}

type fileStore struct {
	options store.Options
	baseDir string

	// the database handle
	sync.RWMutex
	handles map[string]*fileHandle

	defhandle *fileHandle // 指向options.database table
}

func key(database, table string) string {
	return database + ":" + table
}

func (m *fileStore) init(opts ...store.Option) error {
	for _, o := range opts {
		o(&m.options)
	}

	if m.options.Database == "" {
		m.options.Database = DefaultDatabase
	}

	if m.options.Table == "" {
		// bbolt requires bucketname to not be empty
		m.options.Table = DefaultTable
	}

	//context value
	params := make(map[string]string)
	if nil != m.options.Context {
		p := m.options.Context.Value("params")
		if pp, ok := p.(map[string]string); ok {
			params = pp
		}
	}

	//BaseDir
	m.baseDir = DefaultDir
	if value, ok := params["storeBaseDir"]; ok {
		m.baseDir = value
	}

	// create a directory /tmp/micro
	//dir := filepath.Join(m.baseDir, m.options.Database)
	// Ignoring this as the folder might exist.
	// Reads/Writes updates will return with sensible error messages
	// about the dir not existing in case this cannot create the path anyway
	//os.MkdirAll(dir , 0700)

	database := m.options.Database
	table := m.options.Table
	k := key(database, table)
	if nfd, err := newFileHandle(k, m.baseDir, database, table); nil != err {
		log.Error("Open Db Error",
			zap.String("baseDir", m.baseDir),
			zap.String("database", database),
			zap.String("table", table),
			zap.String("Key", k),
			zap.Error(err))
	} else {
		log.Info("Open Db OK",
			zap.String("baseDir", m.baseDir),
			zap.String("database", database),
			zap.String("table", table),
			zap.String("Key", k),
			zap.Error(err))
		m.handles[k] = nfd
		m.defhandle = nfd
	}

	return nil
}

func (m *fileStore) getDB(database, table string) (*fileHandle, error) {
	if 0 == len(database) && 0 == len(table) {
		return m.defhandle, nil
	}

	if len(database) == 0 {
		database = m.options.Database
	}
	if len(table) == 0 {
		table = m.options.Table
	}

	k := key(database, table)
	m.RLock()
	fd, ok := m.handles[k]
	m.RUnlock()

	// return the file handle
	if ok {
		return fd, nil
	}

	// double check locking
	m.Lock()
	defer m.Unlock()
	if fd, ok := m.handles[k]; ok {
		return fd, nil
	}

	//create new db
	fd, err := newFileHandle(k, m.baseDir, database, table)
	if nil == err {
		m.handles[k] = fd
	}
	return fd, err
}

func (m *fileStore) Close() error {
	m.Lock()
	defer m.Unlock()
	for k, v := range m.handles {
		v.db.Close()
		delete(m.handles, k)
	}
	return nil
}

func (m *fileStore) Init(opts ...store.Option) error {
	return m.init(opts...)
}

func (m *fileStore) Delete(key string, opts ...store.DeleteOption) error {
	var deleteOptions store.DeleteOptions
	for _, o := range opts {
		o(&deleteOptions)
	}

	fd, err := m.getDB(deleteOptions.Database, deleteOptions.Table)
	if err != nil {
		return err
	}

	return fd.delete(key)
}

func (m *fileStore) Read(key string, opts ...store.ReadOption) ([]*store.Record, error) {
	var readOpts store.ReadOptions
	for _, o := range opts {
		o(&readOpts)
	}

	fd, err := m.getDB(readOpts.Database, readOpts.Table)
	if err != nil {
		return nil, err
	}

	var keys []string

	// Handle Prefix / suffix
	// TODO: do range scan here rather than listing all keys
	if readOpts.Prefix || readOpts.Suffix {
		// list the keys
		k := fd.list(readOpts.Limit, readOpts.Offset)

		// check for prefix and suffix
		for _, v := range k {
			if readOpts.Prefix && !strings.HasPrefix(v, key) {
				continue
			}
			if readOpts.Suffix && !strings.HasSuffix(v, key) {
				continue
			}
			keys = append(keys, v)
		}
	} else {
		keys = []string{key}
	}

	var results []*store.Record

	for _, k := range keys {
		r, err := fd.get(k)
		if err != nil {
			return results, err
		}
		results = append(results, r)
	}

	return results, nil
}

func (m *fileStore) Write(r *store.Record, opts ...store.WriteOption) error {
	var writeOpts store.WriteOptions
	for _, o := range opts {
		o(&writeOpts)
	}

	fd, err := m.getDB(writeOpts.Database, writeOpts.Table)
	if err != nil {
		return err
	}

	if len(opts) > 0 {
		// Copy the record before applying options, or the incoming record will be mutated
		newRecord := store.Record{}
		newRecord.Key = r.Key
		newRecord.Value = r.Value
		newRecord.Metadata = make(map[string]interface{})
		newRecord.Expiry = r.Expiry

		if !writeOpts.Expiry.IsZero() {
			newRecord.Expiry = time.Until(writeOpts.Expiry)
		}
		if writeOpts.TTL != 0 {
			newRecord.Expiry = writeOpts.TTL
		}

		for k, v := range r.Metadata {
			newRecord.Metadata[k] = v
		}

		return fd.set(&newRecord)
	}

	return fd.set(r)
}

func (m *fileStore) Options() store.Options {
	return m.options
}

func (m *fileStore) List(opts ...store.ListOption) ([]string, error) {
	var listOptions store.ListOptions

	for _, o := range opts {
		o(&listOptions)
	}

	fd, err := m.getDB(listOptions.Database, listOptions.Table)
	if err != nil {
		return nil, err
	}

	// TODO apply prefix/suffix in range query
	allKeys := fd.list(listOptions.Limit, listOptions.Offset)

	if len(listOptions.Prefix) > 0 {
		var prefixKeys []string
		for _, k := range allKeys {
			if strings.HasPrefix(k, listOptions.Prefix) {
				prefixKeys = append(prefixKeys, k)
			}
		}
		allKeys = prefixKeys
	}

	if len(listOptions.Suffix) > 0 {
		var suffixKeys []string
		for _, k := range allKeys {
			if strings.HasSuffix(k, listOptions.Suffix) {
				suffixKeys = append(suffixKeys, k)
			}
		}
		allKeys = suffixKeys
	}

	return allKeys, nil
}

func (m *fileStore) String() string {
	return "file"
}
